Skip to content

Strip the opener from every link a Gadget UI opens - #113

Open
RenKoya1 wants to merge 1 commit into
cloudflare:mainfrom
RenKoya1:fix/gadget-link-noopener
Open

Strip the opener from every link a Gadget UI opens#113
RenKoya1 wants to merge 1 commit into
cloudflare:mainfrom
RenKoya1:fix/gadget-link-noopener

Conversation

@RenKoya1

Copy link
Copy Markdown

The Gadget iframe is sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox", so a link it opens lands in a context that is no longer sandboxed. The capture-phase click handler in INJECTED_CODE_PREFIX is what makes sure that context has no opener.

It matched a[href][target] and then required anchor.target.toLowerCase() === "_blank". Two shapes reach a new context without satisfying that:

  • A link with no target attribute, in a document carrying <base target="_blank">. CSP's base-uri restricts a base element's href, not its target, so Gadget code can set the default target for every link on the page. The anchor's own target attribute stays absent, so it does not match the selector and a.target is "".
  • An SVG anchor. SVGAElement.target is an SVGAnimatedString, so .toLowerCase() throws and the handler never reaches the rel write.

The fix stops trying to decide which links open a new context and matches any anchor. rel=noopener is inert on a same-context navigation, so over-applying it costs nothing, while under-applying it is the failure that matters here. Dropping the .target read removes the SVG hazard as a side effect.

Verification

The handler ships as a string injected into the sandboxed frame, so it is not reachable from the jsdom suite — there is no unit test to add against the shipped code. I ran the two selectors against a jsdom document to confirm the behaviour rather than infer it:

<base target="_blank">
<a id="x" href="https://example.com/">go</a>

OLD  a.target                     = ""
OLD  matches a[href][target]      = false
OLD  rel after click              = null
NEW  rel after click              = "noopener"
NEW  rel after click, on
     <a target="_blank" rel="me"> = "me noopener"

pnpm exec tsc --noEmit clean, workshop-frontend suite 118/118, oxlint reports nothing new.

The Gadget iframe carries allow-popups-to-escape-sandbox, so a link it
opens lands in an unsandboxed context; the injected click handler exists
to make sure that context has no opener. It only matched
`a[href][target]` with `target === "_blank"`, and two shapes slip past:

  - a link with no target attribute of its own, in a document carrying
    `<base target="_blank">`. CSP's base-uri restricts a base element's
    href, not its target, so Gadget code can set the default target for
    every link on the page, and `a.target` stays "".
  - an SVG anchor, whose `.target` is an SVGAnimatedString, so
    `.toLowerCase()` throws and the handler never reaches the rel write.

Match any anchor instead of deciding which ones open a new context.
rel=noopener is inert on a same-context navigation, so over-applying it
costs nothing, and neither shape can slip through.
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@RenKoya1

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant